summaryrefslogtreecommitdiff
path: root/app/[lng]/admin/edp/components/item-selector.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'app/[lng]/admin/edp/components/item-selector.tsx')
-rw-r--r--app/[lng]/admin/edp/components/item-selector.tsx368
1 files changed, 368 insertions, 0 deletions
diff --git a/app/[lng]/admin/edp/components/item-selector.tsx b/app/[lng]/admin/edp/components/item-selector.tsx
new file mode 100644
index 00000000..a81d2ff6
--- /dev/null
+++ b/app/[lng]/admin/edp/components/item-selector.tsx
@@ -0,0 +1,368 @@
+'use client'
+
+import { useState, useEffect } from 'react'
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
+import { Input } from '@/components/ui/input'
+import { Badge } from '@/components/ui/badge'
+import { Search, Check } from 'lucide-react'
+import {
+ ColumnDef,
+ flexRender,
+ getCoreRowModel,
+ getFilteredRowModel,
+ getPaginationRowModel,
+ getSortedRowModel,
+ useReactTable,
+ SortingState,
+ ColumnFiltersState,
+ VisibilityState,
+ RowSelectionState,
+} from '@tanstack/react-table'
+import {
+ Table,
+ TableBody,
+ TableCell,
+ TableHead,
+ TableHeader,
+ TableRow,
+} from '@/components/ui/table'
+import { Checkbox } from '@/components/ui/checkbox'
+import { getItems } from '../actions/data-actions'
+import { toast } from 'sonner'
+import { Item } from '../types/item'
+
+interface ItemSelectorProps {
+ selectedItems: number[]
+ onItemsSelect: (itemIds: number[], itemsData?: Item[]) => void
+ disabled?: boolean
+}
+
+export function ItemSelector({ selectedItems, onItemsSelect, disabled }: ItemSelectorProps) {
+ const [open, setOpen] = useState(false)
+ const [items, setItems] = useState<Item[]>([])
+ const [loading, setLoading] = useState(false)
+ const [sorting, setSorting] = useState<SortingState>([])
+ const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
+ const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({})
+ const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
+ const [globalFilter, setGlobalFilter] = useState('')
+
+ const columns: ColumnDef<Item>[] = [
+ {
+ id: 'select',
+ header: ({ table }) => (
+ <Checkbox
+ checked={table.getIsAllPageRowsSelected()}
+ onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
+ aria-label="Select all"
+ />
+ ),
+ cell: ({ row }) => (
+ <Checkbox
+ checked={row.getIsSelected()}
+ onCheckedChange={(value) => row.toggleSelected(!!value)}
+ aria-label="Select row"
+ />
+ ),
+ enableSorting: false,
+ enableHiding: false,
+ },
+ // 현업 관심 필드들 (우선 순위)
+ {
+ accessorKey: 'ProjectNo',
+ header: '프로젝트 번호',
+ cell: ({ row }) => (
+ <div className="font-mono text-sm font-medium">{row.getValue('ProjectNo')}</div>
+ ),
+ },
+ {
+ accessorKey: 'itemName',
+ header: '아이템명',
+ cell: ({ row }) => (
+ <div className="font-medium max-w-[200px] truncate">{row.getValue('itemName')}</div>
+ ),
+ },
+ {
+ accessorKey: 'packageCode',
+ header: '패키지 코드',
+ cell: ({ row }) => (
+ <div className="font-mono text-sm">{row.getValue('packageCode')}</div>
+ ),
+ },
+ // 추가 필드들
+ {
+ accessorKey: 'itemCode',
+ header: '아이템 코드',
+ cell: ({ row }) => {
+ const code = row.getValue('itemCode') as string | null
+ return code ? (
+ <div className="font-mono text-sm">{code}</div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'description',
+ header: '설명',
+ cell: ({ row }) => {
+ const description = row.getValue('description') as string | null
+ return description ? (
+ <div className="max-w-[250px] truncate text-sm">{description}</div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'unitOfMeasure',
+ header: '단위',
+ cell: ({ row }) => {
+ const unit = row.getValue('unitOfMeasure') as string | null
+ return unit ? (
+ <Badge variant="outline" className="text-xs">{unit}</Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'smCode',
+ header: 'SM 코드',
+ cell: ({ row }) => {
+ const code = row.getValue('smCode') as string | null
+ return code ? (
+ <div className="font-mono text-xs">{code}</div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'steelType',
+ header: '강종',
+ cell: ({ row }) => {
+ const steelType = row.getValue('steelType') as string | null
+ return steelType ? (
+ <Badge variant="secondary" className="text-xs">{steelType}</Badge>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ {
+ accessorKey: 'itemLevel',
+ header: '레벨',
+ cell: ({ row }) => {
+ const level = row.getValue('itemLevel') as number | null
+ return level !== null ? (
+ <div className="text-sm text-center">{level}</div>
+ ) : (
+ <span className="text-muted-foreground">-</span>
+ )
+ },
+ },
+ ]
+
+ const table = useReactTable({
+ data: items,
+ columns,
+ onSortingChange: setSorting,
+ onColumnFiltersChange: setColumnFilters,
+ onColumnVisibilityChange: setColumnVisibility,
+ onRowSelectionChange: setRowSelection,
+ onGlobalFilterChange: setGlobalFilter,
+ getCoreRowModel: getCoreRowModel(),
+ getPaginationRowModel: getPaginationRowModel(),
+ getSortedRowModel: getSortedRowModel(),
+ getFilteredRowModel: getFilteredRowModel(),
+ state: {
+ sorting,
+ columnFilters,
+ columnVisibility,
+ rowSelection,
+ globalFilter,
+ },
+ initialState: {
+ pagination: {
+ pageSize: 10,
+ },
+ },
+ })
+
+ const loadItems = async () => {
+ setLoading(true)
+ try {
+ const result = await getItems()
+ if (result.success && result.data) {
+ setItems(result.data)
+ } else {
+ toast.error(result.error)
+ }
+ } catch {
+ toast.error('아이템을 불러오는 중 오류가 발생했습니다.')
+ } finally {
+ setLoading(false)
+ }
+ }
+
+ const handleConfirmSelection = () => {
+ const selectedRows = table.getFilteredSelectedRowModel().rows
+ const selectedItemIds = selectedRows.map(row => row.original.id)
+ const selectedItemsData = selectedRows.map(row => row.original)
+ onItemsSelect(selectedItemIds, selectedItemsData)
+ setOpen(false)
+ }
+
+ // 선택된 아이템 수 계산
+ const selectedCount = Object.keys(rowSelection).length
+
+ useEffect(() => {
+ if (open && items.length === 0) {
+ loadItems()
+ }
+ }, [open])
+
+ // 기존 선택 상태를 rowSelection에 반영
+ useEffect(() => {
+ if (items.length > 0 && selectedItems.length > 0) {
+ const newRowSelection: RowSelectionState = {}
+ items.forEach((item, index) => {
+ if (selectedItems.includes(item.id)) {
+ newRowSelection[index] = true
+ }
+ })
+ setRowSelection(newRowSelection)
+ }
+ }, [items, selectedItems])
+
+ return (
+ <Dialog open={open} onOpenChange={setOpen}>
+ <DialogTrigger asChild>
+ <Button variant="outline" disabled={disabled} className="w-full justify-start">
+ {selectedItems.length > 0 ? (
+ <span>{selectedItems.length}개 아이템 선택됨</span>
+ ) : (
+ <span className="text-muted-foreground">아이템을 선택하세요</span>
+ )}
+ </Button>
+ </DialogTrigger>
+ <DialogContent className="max-w-7xl max-h-[90vh]">
+ <DialogHeader>
+ <DialogTitle>아이템 선택</DialogTitle>
+ </DialogHeader>
+
+ <div className="space-y-4">
+ <div className="flex items-center justify-between">
+ <div className="flex items-center space-x-2 flex-1">
+ <Search className="h-4 w-4" />
+ <Input
+ placeholder="프로젝트 번호, 아이템명, 패키지 코드, 설명으로 검색..."
+ value={globalFilter}
+ onChange={(e) => setGlobalFilter(e.target.value)}
+ className="flex-1"
+ />
+ </div>
+ <div className="text-sm text-muted-foreground ml-4">
+ {selectedCount}개 선택됨
+ </div>
+ </div>
+
+ {loading ? (
+ <div className="flex justify-center py-8">
+ <div className="text-sm text-muted-foreground">아이템을 불러오는 중...</div>
+ </div>
+ ) : (
+ <div className="border rounded-md max-h-[60vh] overflow-auto">
+ <Table>
+ <TableHeader>
+ {table.getHeaderGroups().map((headerGroup) => (
+ <TableRow key={headerGroup.id}>
+ {headerGroup.headers.map((header) => (
+ <TableHead key={header.id} className="sticky top-0 bg-background">
+ {header.isPlaceholder
+ ? null
+ : flexRender(
+ header.column.columnDef.header,
+ header.getContext()
+ )}
+ </TableHead>
+ ))}
+ </TableRow>
+ ))}
+ </TableHeader>
+ <TableBody>
+ {table.getRowModel().rows?.length ? (
+ table.getRowModel().rows.map((row) => (
+ <TableRow
+ key={row.id}
+ data-state={row.getIsSelected() && "selected"}
+ className="cursor-pointer hover:bg-muted/50"
+ onClick={() => row.toggleSelected()}
+ >
+ {row.getVisibleCells().map((cell) => (
+ <TableCell key={cell.id} className="py-2">
+ {flexRender(
+ cell.column.columnDef.cell,
+ cell.getContext()
+ )}
+ </TableCell>
+ ))}
+ </TableRow>
+ ))
+ ) : (
+ <TableRow>
+ <TableCell
+ colSpan={columns.length}
+ className="h-24 text-center"
+ >
+ 검색 결과가 없습니다.
+ </TableCell>
+ </TableRow>
+ )}
+ </TableBody>
+ </Table>
+ </div>
+ )}
+
+ <div className="flex items-center justify-between">
+ <div className="text-sm text-muted-foreground">
+ 총 {table.getFilteredRowModel().rows.length}개 아이템
+ </div>
+ <div className="flex items-center space-x-2">
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => table.previousPage()}
+ disabled={!table.getCanPreviousPage()}
+ >
+ 이전
+ </Button>
+ <div className="text-sm">
+ {table.getState().pagination.pageIndex + 1} / {table.getPageCount()}
+ </div>
+ <Button
+ variant="outline"
+ size="sm"
+ onClick={() => table.nextPage()}
+ disabled={!table.getCanNextPage()}
+ >
+ 다음
+ </Button>
+ </div>
+ </div>
+
+ <div className="flex justify-end space-x-2 pt-4 border-t">
+ <Button variant="outline" onClick={() => setOpen(false)}>
+ 취소
+ </Button>
+ <Button onClick={handleConfirmSelection} disabled={selectedCount === 0}>
+ {selectedCount}개 아이템 선택 확인
+ </Button>
+ </div>
+ </div>
+ </DialogContent>
+ </Dialog>
+ )
+}